链接
本章的核心内容:
- 如何把多个源代码文件合并成一个最终可执行的文件
- 符号(变量,函数等)的定义和声明在合并过程中的表现形式
- 了解
链接
过程中的符号解析
和重定位
的作用 - 了解
静态链接库
和动态链接库
的区别
编译器驱动程序
示例代码:
源代码转换成可执行代码的步骤?
- 一步到位:gcc -Og -o prog main.c sum.c
- 预处理:gcc cpp main.c /tmp/main.i
- 编译:cc1 /tmp/main.i -Og -o /tmp/main.s
- 汇编:as -o /tmp/main.o /tmp/main.s
- 链接:ld -o prog /tmp/main.o /tmp/sum.o
链接在其中的作用?
把多个源代码文件合并成一个最终可执行的文件
静态链接
链接器必须完成的两个主要任务?
- 符号解析
- 重定位
现在不理解没关系,之后会详细的说明
目标文件
目标文件有哪些类?
- 可重定位目标文件:需要在链接时和其他的可重定位目标文件合并为一个可执行的目标文件。
- 可执行的目标文件:可以直接加载到内存并执行
- 共享目标文件:一种特殊的可重定位目标文件,可以在加载或者运行时被动态的加载到内存中进行链接。
可重定位目标文件
典型的可重新定位的目标文件(ELF)的格式?
符号和符号表
符号表的意义?
对于机器而言,没有变量和函数的区别,统称为符号。
所有在目标模块中引用到的符号都会记录在符号表中,同时关联该符号的定义位置(或者未定义)
符号表在哪?都有哪些信息?
符号表在.symtab节中
包含所有在目标模块中引用的符号
不包含局部变量
符号表中的符号都有哪些类型?
- 全局符号:当前模块定义,可被其他模块引用
- 外部符号:其他模块定义,被当前模块引用
- 局部符号:由static修饰的全局变量,当前模块定义,不可被其他模块引用
符号表中每一条的格式?
section说明符号在哪一节中
有三个虚拟的节:
- UNDEF:未定义的符号
- COMMON:未初始化的符号
- ABS:不该被重定位的符号
案例:readelf命令可以查看文件符号表
符号解析
什么是符号解析?为什么需要符号解析?
由于引用了外部定义的符号,所以在链接的时候需要从其他模块中寻找到符号的定义位置,并且关联起来
这个过程就叫做符号解析
如何解析多重定义的全局变量?
意思是:如果符号在多个地方被定义,如何解析?
根据符号的定义方式,分为:
- 强符号:函数和已初始化的全局变量,或者未初始化的static符号
- 弱符号:未初始化的全局变量
解析规则如下:
- 不允许有多个同名的强符号
- 如果有一个强符号和多个弱符号同名,选择强符号
- 如果有多个弱符号,任意选择一个
符号表中section属性中COMMON和.bss区别体现在?
COMMON:未初始化的全局变量,为弱符号
.bss:初始化为0的全局变量或者static变量,为强符号
规则2和规则3造成的潜藏错误?
未使用本模块中定义的符号,而修改了其他模块的符号
造成了未知的数据错误
什么是静态库?为什么需要静态库?
将所有相关的模块打包成一个单独的文件,称为静态库
如果没有静态库:
- 每次都需要链接很多个模块,容易出错而且耗时
- 链接一个超大的模块(只会其中的一小部分),占用了不必要的空间,同样也耗时
而使用静态库,目标文件会去静态库中找自己需要的模块
如何创建静态库?
使用ar命令,静态库以.a表示
ar rcs my.a main.o sum.o
如何利用静态库来进行链接(链接的步骤)?
链接器从左到右扫描所有的目标文件,维护三个集合:
- 文件集合E:集合中的文件会合并
- 未解析的符号集合U
- 已定义的符号集合D
链接过程:
- 对于每个输入文件f,如果是可重定位文件,则添加到E中,修改U和D
- 如果是静态库,则尝试匹配U中的符号,如果匹配到,则把对应的模块加入到E中,修改U和D
- 结束时,如果U非空,则报错。否则,重定位E中的目标文件
链接静态库的顺序问题?
链接器从左到右扫描,因此要确保需要的符号在后续目标文件中出现
重定位
为什么需要重定位?
未合并的代码和数据内存位置都是不确切的,因此指令中的内存地址以及数据节的内存位置也无法确定
重定位的目的就是合并代码,并且确定内存地址重定位条目
就是所有内存需要确定的地点
重定位的步骤?
- 重定位节和符号定义:根据合并时的模块节和符号定义的相对未知进行重定位
- 重定位所有的符号引用:根据
重定位条目
进行重定位
重定位的地址计算?
ELF有32种多重定位类型,比如:
- R_X86_64_PC32:重定位一个使用32位PC相对地址的引用
- R_X86_64_32:重定位一个使用32位绝对地址的引用
可执行文件
可执行文件的格式?
可执行文件加载后的内存分布?
加载可执行文件
加载器是内核代码,任何程序都可以通过execve函数调用。
- 加载器创建虚拟空间
- 将可执行文件的片复制到代码段和数据段
- 加载器跳转到程序的入口_start函数(ctrl.o中定义)
- _start函数调用系统启动函数__libc_start_main函数(libc.so中定义)
- __libc_start_main函数初始化执行环境,调用用户层的Main函数
要理解加载的实际工作,需要理解进程、虚拟内存和内存映射的概念
动态链接库
静态库的弊端?
- 静态库需要定期维护和更新,因此必须以某种方式了解到静态库的更新状态,然后显式的重新链接
- 对于每一个程序都会使用到的函数,比如printf和scanf,每一个进程都会去保存这一部分代码,这是对内存的一种浪费。
什么是动态共享库?
一种特殊的目标模块,在程序加载或者运行时,加载到任意的内存地址,并与程序进行链接,是所有程序都可以共享的一段内存地址。
如何创建动态共享库和使用动态共享库?
动态共享库文件以.so结尾
可以通过命令来进行创建和使用
如何显式地使用动态共享库?
通过命令进行动态链接是隐式的,将符号解析全部交给动态链接器,在程序加载到内存之后就完成符号解析。
在代码中声明链接称为显式链接,符号解析需要手动调用动态链接器开放出来的接口,在接口执行时才进行符号解析。
显式链接最大的好处就是:即便动态共享库更新了,也无需重新编译整个程序。
在目标模块变成可执行文件后,其指令就不允许被修改了,那么这种执行后(加载、运行)才进行链接的方式是如何实现的?
位置无关代码
动态链接共享库有哪些难点?
最大的难点是动态共享库在内存中存在的位置
如果给动态共享库预留专用的地址空间,问题是即使这部分空间没被用到,也会被无故占用浪费。并且随着更新迭代不好管理。
如何解决上述问题?
需要一种方式,是的无论共享库放在内存中的哪个位置,它的代码段都可以被多个进程共享
这种链接时无需重定位,而可以加载的代码就是位置无关代码(PIC)
无需重定位意味着什么?
不进行重定位意味着无法在链接时就确定内存地址(虚拟内存)
位置无关代码需要解决的问题?
共享模块只有加载后内存地址才能确定
也就是说共享模块只能将重定位(动态重定位)的过程放到加载之后
PIC加载之后如何进行重定位,正是目前需要解决的问题
PIC数据如何被引用?
PIC的实现基于数据段和代码段的距离总是不变以及数据段可以被修改这两个特性
在数据段中创建一个表——全局偏移量表(GOT)
——因而让表项的地址来作为“代替地址”
因为常见情形是PIC中引用其他PIC,所以这个“代替地址”一般也无法确定内存地址(绝对地址)
在链接时,如果发现引用了一个PIC中的数据,则先使用一个“代替地址”,这个“代替地址”最终一定能定位到正确的PIC数据地址
GOT表中何时得到正确的地址呢?
在生成GOT表项的同时,会生成一条动态重定位项
在加载时,动态链接器会根据动态重定位项去获取正确的PIC数据地址
PIC函数如何被引用?
也需要使用到GOT表
除此之外,为了实现延迟加载(第一次调用时才加载),还需要用到过程链接表(PLT)
库打桩机制
库打桩有什么用?